kraco.dev

Sign In
Creating a mental health web application for the IFS community

Internal Family Systems (IFS) therapy is a psychotherapeutic model that helps individuals understand and navigate their inner world by identifying and interacting with different sub-personalities, or "parts." In this project I worked with an IFS Level 3 certified therapist and my business partner to develop a web application that enhances this therapeutic process by allowing users to map their "Internal Systems". The project took around six months and is now in open beta. Throughout this project, I wore many hats, but my primary role was as the engineer and developer. I used Next.js, Supabase, Reactflow, and Sanity.io for the core features of the web application.

The application file hierarchy was fairly straightforward.

I chose Next.js for its SSR capabilities, particularly for the blog portion of the site, where content is pulled from an external CMS (Sanity.io). To achieve this, I developed a page.tsx component that fetches data from Sanity before rendering.

TSX
1export const metadata = getSEOTags({
2  title: `Blog | ${config.appName}`,
3  canonicalUrlRelative: "/blog",
4  description: "Sharing app progress, personal journeys, and more.",
5});
6
7export default async function Page() {
8  const posts = await sanityFetch<SanityDocument[]>({
9    query: postsQuery,
10    tags: ["post"],
11  });
12  const isDraftMode = isDraftModeTrue();
13  if (isDraftMode && token) {
14    console.log("previewing content");
15    return (
16      <main className="flex flex-col gap-12 px-8 py-24">
17        <PreviewProvider token={token}>
18          <PreviewPosts posts={posts} />
19        </PreviewProvider>
20      </main>
21    );
22  }
23  return (
24    <main className="flex flex-col gap-12 px-8 py-24">
25      <Posts posts={posts} />
26    </main>
27  );
28}

Another key part of the application was user authentication, which I implemented using Supabase middleware and a layout.tsx component.

TSX
1export default async function LayoutPrivate({
2  children,
3}: {
4 children: ReactNode;
5}) {
6 const supabase = await createClient();
7 const {
8 data: { user },
9 } = await supabase.auth.getUser();
10 if (!user) {
11 redirect("/");
12 }
13  ...

For the core functionality—namely, user parts/network mapping—I used Reactflow.dev for rendering nodes and edges, along with React Query for server synchronization.

TSX
1if (userParts.data) {
2    return (
3      <div
4        className="reactflow-wrapper h-full w-full overscroll-contain"
5        ref={reactFlowWrapper}
6      >
7        <ReactFlow
8          nodeTypes={nodeTypes}
9          onInit={tick}
10          edgeTypes={edgeTypes}
11          deleteKeyCode={["Backspace", "Delete"]}
12          nodes={nodes}
13          edges={edges}
14          onNodesChange={onNodesChange}
15          onEdgesChange={onEdgesChange}
16          onDragOver={onDragOver}
17          onDrop={onDrop}
18          zoomOnDoubleClick={false}
19          zoomOnPinch={true}
20          connectionLineType={ConnectionLineType.Straight}
21          connectionLineStyle={{
22            stroke: `oklch(var(--ec)) !important`,
23            strokeWidth: 3,
24          }}
25          defaultEdgeOptions={{
26            style: { strokeWidth: 3, stroke: `oklch(var(--ec)) !important` },
27            type: "contextFloatingEdge",
28            label: "Protecting",
29            data: "protective_relationships",
30            markerEnd: {
31              type: MarkerType.ArrowClosed,
32            },
33          }}
34          onConnect={onConnect}
35          onEdgesDelete={(e) => {
36            for (const edge of e) {
37              deleteRelationship.mutate(edge.id);
38            }
39          }}
40          onNodesDelete={(n) => {
41            deletePartsFromContext.mutate(
42              n.map((node) => {
43                return { part_id: node.id, context_id: context_id };
44              }),
45            );
46          }}
47          panOnScroll
48          panOnDrag={[1, 2]}
49          selectionOnDrag={true}
50          selectionMode={SelectionMode.Partial}
51          onPaneClick={(e) => {
52            if (e.ctrlKey) {
53              addNewPart(e);
54            } else {
55              mapStore.setSelectedPartId(null);
56              mapStore.setSelectedInto(null);
57            }
58          }}
59       
60        ...

For designing the React Query hooks, I followed the guidelines set up in TkDodo's blog (https://tkdodo.eu/blog/practical-react-query).

The database was also a crucial part of the project. Getting the table structures and relationships right early on was essential to avoid constant redesigns as new requirements emerged.

I primarily used Supabase for its convenient JavaScript library for querying, but most of the table and view design was done purely through SQL.

For example, the enriched_feelings_tree view was built to provide a data-drillable tree for user surveys.

SQL
1create view
2  public.enriched_feelings_tree as
3with recursive
4  t as (
5    select
6      f.id,
7      null::bigint as parent_id,
8      0 as depth
9    from
10      feelings f
11      join feelings_tree ft on ft.feeling_id = f.id
12      and ft.parent_id is null
13    union all
14    select
15      ft.feeling_id as id,
16      ft.parent_id,
17      t_1.depth + 1
18    from
19      feelings_tree ft
20      join t t_1 on t_1.id = ft.parent_id
21  )
22select
23  f1.id as feeling_id,
24  f1.nvc,
25  f1.feeling,
26  f2.id as parent_feeling_id,
27  t.depth
28from
29  t
30  left join feelings f1 on f1.id = t.id
31  left join feelings f2 on f2.id = t.parent_id;

This project was more than just an exercise in software development—it was an opportunity to contribute to a tool that could make a meaningful impact on therapeutic practices. By integrating modern web technologies with the IFS model, I helped create a tool that enhances self-exploration and healing. Seeing the application go from concept to open beta has been incredibly rewarding, and I look forward to how it evolves as more users engage with it.